﻿using Fuse8.ContentManagementFramework.BizFacade.Services;
using Fuse8.ContentManagementFramework.Domain;
using Fuse8.ContentManagementFramework.Domain.Special;
using Fuse8.ContentManagementFramework.SystemFacade;
using Fuse8.ContentManagementFramework.SystemFacade.Extensions;
using Fuse8.DomainFramework.BusinessService;
using Fuse8.DomainFramework.Common;
using Fuse8.DomainFramework.Common.Locator;
using Fuse8.DomainFramework.Common.Utilities;
using Fuse8.DomainFramework.Domain;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;

namespace Fuse8.ContentManagementFramework.BizFacade.Managers
{
    public class CmsSiteImportExportManager
    {
        #region Fields

        private readonly CmsSiteService _siteService = new CmsSiteService();
        private readonly BusinessServiceFacade<CmsSiteGroup, int> _siteGroupService = new BusinessServiceFacade<CmsSiteGroup, int>();
        private readonly CmsContentTypeService _contentTypeService = new CmsContentTypeService();
        private readonly CmsContentService _contentService = new CmsContentService();
        private readonly CmsTemplateService _templateService = new CmsTemplateService();
        private readonly BusinessServiceFacade<CmsTemplateRegion, int> _templateRegionService = new BusinessServiceFacade<CmsTemplateRegion, int>();
        private readonly CmsMediaService _mediaService = new CmsMediaService();
        private readonly CmsMediaContentService _mediaContentService = new CmsMediaContentService();
        private readonly CmsMediaFileService _mediaFileService = new CmsMediaFileService();
        private readonly CmsModuleService _moduleService = new CmsModuleService();
        private readonly CmsPageService _pageService = new CmsPageService();
        private readonly CmsPageControlService _pageControlService = new CmsPageControlService();
        private readonly CmsMimeTypeService _mimeTypeService = new CmsMimeTypeService();
        private readonly CmsEntityMetaFieldService _metaFieldService = new CmsEntityMetaFieldService();
        private readonly CmsEntityMetaFieldLinkService _metaFieldLinkService = new CmsEntityMetaFieldLinkService();
        private readonly BusinessServiceFacade<CmsEntityMeta, int> _metaService = new BusinessServiceFacade<CmsEntityMeta, int>();

        private readonly BusinessServiceFacade<CmsMediaContentFileLink, int> _mediaContentFileLinkService =
            new BusinessServiceFacade<CmsMediaContentFileLink, int>();

        private ImportMap<CmsMimeType> _mimeTypeMap;
        private ImportMap<CmsTemplateRegion> _templateRegionsMap;
        private ImportMap<CmsTemplate> _templatesMap;
        private ImportMap<CmsContentType> _contentTypesMap;
        private ImportMap<CmsMedia> _mediaMap;
        private ImportMap<CmsMediaContent> _mediaContentsMap;
        private ImportMap<CmsMediaFile> _mediaFilesMap;
        private ImportMap<CmsModule> _modulesMap;
        private ImportMap<CmsPage> _pagesMap;
        private ImportMap<CmsContent> _contentsMap;
        private ImportMap<CmsPageControl> _pageControlsMap;

        private CmsSite _site;

        #endregion Fields

        protected ICmsContext CmsContext
        {
            get { return DependencyServiceLocator.Current.GetInstance<ICmsContext>(); }
        }

        public byte[] Export(CmsSite site)
        {
            Guard.ArgumentNotNull(site, "site");
            var siteEntities = ExportEntities(site);

            byte[] result = null;

            using (MemoryStream stream = new MemoryStream())
            {
                using (GZipStream zipStream = new GZipStream(stream, CompressionMode.Compress, true))
                {
                    result = Encoding.UTF8.GetBytes(siteEntities.StoreMe());

                    zipStream.Write(result, 0, result.Length);
                }

                result = stream.ToArray();
            }

            return result;
        }

        public CmsSite Import(byte[] bytes)
        {
            Guard.ArgumentNotNull(bytes, "bytes");

            string serializedEntity = string.Empty;
            using (MemoryStream memory = new MemoryStream(bytes))
            {
                memory.Position = 0;

                using (GZipStream gZip = new GZipStream(memory, CompressionMode.Decompress))
                {
                    byte[] decompressed = new byte[0];

                    byte[] unzipBuffer = new byte[512];

                    while (true)
                    {
                        int bytesRead = gZip.Read(unzipBuffer, 0, unzipBuffer.Length);
                        if (bytesRead == 0)
                            break;

                        Array.Resize(ref decompressed, decompressed.Length + bytesRead);
                        Array.Copy(unzipBuffer, 0, decompressed, decompressed.Length - bytesRead, bytesRead);
                    }

                    serializedEntity = Encoding.UTF8.GetString(decompressed);
                }
            }

            var siteEntities = serializedEntity.DigUpFromString<CmsSiteEntities>();
            return ImportEntities(siteEntities);
        }

        public CmsSite Clone(CmsSite site, string newSiteName)
        {
            Guard.ArgumentNotNull(site, "site");
            Guard.ArgumentNotNull(newSiteName, "newSiteName");

            var siteEntities = ExportEntities(site);
            var serializedEntities = siteEntities.StoreMe();
            var deserializedEntities = serializedEntities.DigUpFromString<CmsSiteEntities>();
            var newSite = ImportEntities(deserializedEntities);
            newSite.SiteName = newSiteName;
            _siteService.Save(newSite);
            return newSite;
        }

        #region Private members

        private CmsSiteEntities ExportEntities(CmsSite site)
        {
            Guard.ArgumentNotNull(site, "site");

            var entities = new CmsSiteEntities();

            var previousContextSite = CmsContext.ContextSite;

            CmsContext.ContextSite = site;

            entities.Site = site;

            entities.SiteGroup = site.SiteGroup;

            ExportCmsContents(site, entities);

            ExportCmsTemplates(site, entities);

            ExportCmsMedia(site, entities);

            ExportCmsModules(site, entities);

            ExportCmsPages(site, entities);

            ExportCmsMetas(entities);

            CmsContext.ContextSite = previousContextSite;

            return entities;
        }

        private CmsSite ImportEntities(CmsSiteEntities siteEntities)
        {
            Guard.ArgumentNotNull(siteEntities, "siteEntities");
            Guard.ArgumentNotNull(siteEntities.Site, "siteEntities.Site");
            Guard.ArgumentNotNull(siteEntities.SiteGroup, "siteEntities.SiteGroup");

            using (var transaction = new BusinessServiceTransaction<CmsSiteGroup>().With<CmsSite>()
                .With<CmsContentType>().With<CmsTemplate>().With<CmsTemplateRegion>()
                .With<CmsMedia>().With<CmsMediaContent>().With<CmsMediaFile>().With<CmsMediaContentFileLink>()
                .With<CmsMimeType>().With<CmsContent>().With<CmsPage>()
                .With<CmsPageControl>().With<CmsModule>().With<CmsEntityMetaField>()
                .With<CmsEntityMetaFieldLink>().With<CmsEntityMeta>().With<CmsEntityMetaInformation>())
            {
                var previousContextSite = CmsContext.ContextSite;
                CmsContext.UseBuiltInFlow = false;

                try
                {
                    var site = ImportCmsSite(siteEntities);

                    CmsContext.ContextSite = site;

                    ImportCmsContent(siteEntities);

                    ImportCmsTemplates(siteEntities);

                    ImportCmsMedia(siteEntities);

                    ImportCmsModules(siteEntities);

                    ImportCmsPages(siteEntities);

                    ImportCmsMeta(siteEntities);

                    transaction.Commit();

                    return site;
                }
                catch (Exception)
                {
                    transaction.Rollback();
                    throw;
                }
                finally
                {
                    CmsContext.ContextSite = previousContextSite;
                    CmsContext.UseBuiltInFlow = true;
                }
            }
        }

        #region Private members: Export entity procedures

        private static void ExportCmsMetas(CmsSiteEntities entities)
        {
            entities.AppendEntityMetaInformation(entities.Site.EntityMetaInformation);
            entities.AppendEntityMetaInformation(entities.Pages.Select(page => page.EntityMetaInformation));
        }

        private void ExportCmsPages(CmsSite site, CmsSiteEntities entities)
        {
            entities.Pages = _pageService.FindAll().Where(e => e.SiteId == site.Id).ToList();

            entities.PageControls = entities.Pages.SelectMany(page => page.PageControls).ToList();
        }

        private void ExportCmsModules(CmsSite site, CmsSiteEntities entities)
        {
            entities.Modules = _moduleService.FindAll().ToList();
        }

        private void ExportCmsContents(CmsSite site, CmsSiteEntities entities)
        {
            entities.ContentTypes = _contentTypeService.FindAll().ToList();

            entities.Contents = _contentService.FindAll().Where(e => e.SiteId == site.Id).ToList();
        }

        private void ExportCmsTemplates(CmsSite site, CmsSiteEntities entities)
        {
            entities.Templates = _templateService.FindAll().ToList();

            entities.TemplateRegions = entities.Templates.SelectMany(template => template.TemplateRegions).ToList();
        }

        private void ExportCmsMedia(CmsSite site, CmsSiteEntities entities)
        {
            entities.Media = FilterCmsMediaTreeBySiteId(_mediaService.FindAllWithSubfolders(), site.Id).ToList();
            entities.MediaContents = new List<CmsMediaContent>();
            entities.MediaFiles = new List<CmsMediaFile>();
            entities.MediaContentFileLinks = new List<CmsMediaContentFileLink>();

            foreach (var media in entities.Media)
            {
                var mediaDetails = _mediaService.FindOne(media.Id);

                if (mediaDetails.MediaContents != null && mediaDetails.MediaContents.Any())
                {
                    entities.MediaContents.AddRange(mediaDetails.MediaContents);

                    entities.MediaFiles.AddRange(mediaDetails.MediaContents.SelectMany(e => e.LinkedFiles));

                    entities.MediaContentFileLinks.AddRange(mediaDetails.MediaContents
                    .SelectMany(mediaContent => mediaContent.LinkedFiles
                        .Select(mediaFile =>
                                new CmsMediaContentFileLink
                                {
                                    MediaFileId = mediaFile.Id,
                                    MediaContentId = mediaContent.Id,
                                    IsPrimary = mediaFile.IsPrimary
                                })));
                }
            }
        }

        // This method returns a list of CmsMedia, filtered by SiteId including subfolders.
        private static IEnumerable<CmsMedia> FilterCmsMediaTreeBySiteId(IEnumerable<CmsMedia> mediaItems, int siteId)
        {
            var result = new List<CmsMedia>();
            foreach (var mediaItem in mediaItems.OrderBy(x => x.ParentMediaId))
            {
                if (mediaItem.SiteId == null || mediaItem.SiteId == siteId)
                {
                    if ((mediaItem.SiteId == null && result.Any(x => x.Id == mediaItem.ParentMediaId)) || mediaItem.SiteId == siteId)
                    {
                        result.Add(mediaItem);
                    }
                }
            }
            return result;
        }

        // This method converts a CmsMedia list in the tree.
        public IEnumerable<CmsMedia> TransformToTree(IEnumerable<CmsMedia> mediaItems)
        {
            var result = mediaItems.OrderBy(x => x.ParentMediaId).ToList();
            var count = result.Count();
            for (int i = count - 1; i >= 0; i--)
            {
                var element = result[i];
                var parent = result.FirstOrDefault(x => x.Id == element.ParentMediaId);
                if (parent == null) continue;
                if (parent.SubFolders == null)
                {
                    parent.SubFolders = new List<CmsMedia>();
                }
                var temp = parent.SubFolders.ToList();
                temp.Add(element);
                parent.SubFolders = temp;
                result.Remove(element);
            }
            return result;
        }

        #endregion Private members: Export entity procedures

        #region Private members: Import entity methods

        private void ImportCmsMeta(CmsSiteEntities siteEntities)
        {
            // Meta Field
            var metaFieldsMap = new ImportMap<CmsEntityMetaField>();

            foreach (var metaField in siteEntities.MetaFields)
            {
                var newMetaField = _metaFieldService.FindByName(metaField.MetaFieldName, metaField.MetaFieldType);

                if (newMetaField == null)
                {
                    newMetaField = CloneEntity(metaField);

                    _metaFieldService.Add(newMetaField);
                }

                metaFieldsMap[metaField.Id] = newMetaField.Id;
            }

            var metaFieldLinksMap = new ImportMap<CmsEntityMetaFieldLink>();

            // Meta Field Links
            foreach (var metaFieldLink in siteEntities.MetaFieldLinks)
            {
                var newMetaFieldLink = _metaFieldLinkService.FindForEntityType(metaFieldLink.EntityTypeParameter)
                    .SingleOrDefault(e => e.EntityMetaFieldId == metaFieldsMap[metaFieldLink.EntityMetaFieldId]);

                if (newMetaFieldLink == null)
                {
                    newMetaFieldLink = CloneEntity(metaFieldLink);

                    newMetaFieldLink.EntityMetaFieldId = metaFieldsMap[metaFieldLink.EntityMetaFieldId];

                    _metaFieldLinkService.Add(newMetaFieldLink);
                }

                metaFieldLinksMap[metaFieldLink.Id] = newMetaFieldLink.Id;
            }

            // Metas
            foreach (var meta in siteEntities.Metas)
            {
                var metaFieldLink = siteEntities.MetaFieldLinks
                    .SingleOrDefault(e => e.Id == meta.EntityMetaFieldLinkId);

                if (metaFieldLink == null) continue;

                var newMeta = CloneEntity(meta);
                newMeta.EntityMetaFieldLinkId = metaFieldLinksMap[meta.EntityMetaFieldLinkId];

                // mapping EntityId depending on EntityType
                // TODO possible solution: save metas along with entity objects OR create a separate dictionary
                if (metaFieldLink.EntityTypeParameter == CmsConstants.PageTypeParameter)
                {
                    newMeta.EntityId = _pagesMap[meta.EntityId];
                }
                else if (metaFieldLink.EntityTypeParameter == CmsConstants.SiteTypeParameter)
                {
                    newMeta.EntityId = _site.Id;
                }

                _metaService.Add(newMeta);
            }
        }

        private void ImportCmsPages(CmsSiteEntities siteEntities)
        {
            // Pages
            _pagesMap = new ImportMap<CmsPage>();
            foreach (var page in siteEntities.Pages)
            {
                var newPage = CloneEntity(page);

                newPage.ParentPageId =
                    page.ParentPageId.HasValue
                        ? _pagesMap[page.ParentPageId.Value]
                        : new int?();

                newPage.TemplateId = _templatesMap[page.TemplateId];

                // adding meta fields for page validation
                var metaLinks = siteEntities.MetaFieldLinks.Where(e => e.EntityTypeParameter == newPage.TypeParameter).Select(e => e.Id);
                var metas =
                    siteEntities.Metas.Where(e => metaLinks.Contains(e.EntityMetaFieldLinkId) && e.EntityId == page.Id).
                        ToList();
                newPage.EntityMetaInformation = new CmsEntityMetaInformation() { Metas = metas };

                _pageService.Add(newPage);

                _pagesMap[page.Id] = newPage.Id;
            }

            // PageControls
            _pageControlsMap = new ImportMap<CmsPageControl>();
            foreach (var pageControl in siteEntities.PageControls)
            {
                var newPageControl = CloneEntity(pageControl);

                newPageControl.PageId = _pagesMap[pageControl.PageId];
                newPageControl.TemplateRegionId = _templateRegionsMap[pageControl.TemplateRegionId];
                newPageControl.ModuleId =
                    pageControl.ModuleId.HasValue
                        ? _modulesMap[pageControl.ModuleId.Value]
                        : new int?();
                newPageControl.PageControlContentId =
                    pageControl.PageControlContentId.HasValue
                        ? _contentsMap[pageControl.PageControlContentId.Value]
                        : new int?();

                _pageControlService.Add(newPageControl);

                _pageControlsMap[pageControl.Id] = newPageControl.Id;
            }
        }

        private void ImportCmsModules(CmsSiteEntities siteEntities)
        {
            // Modules
            _modulesMap = new ImportMap<CmsModule>();
            foreach (var module in siteEntities.Modules)
            {
                var newModule = _moduleService.FindAll()
                    .SingleOrDefault(e => e.ModuleName == module.ModuleName && e.PackageId == module.PackageId);

                if (newModule == null)
                {
                    newModule = CloneEntity(module);

                    _moduleService.Add(newModule);
                }

                _modulesMap[module.Id] = newModule.Id;
            }
        }

        private void InsertSubFolders(CmsMedia media)
        {
            if (media.SubFolders != null && media.SubFolders.Any())
            {
                foreach (var subFolder in media.SubFolders)
                {
                    var newMedia = CloneEntity(subFolder);
                    newMedia.ParentMediaId = media.Id;
                    _mediaService.Add(newMedia);
                    _mediaMap[subFolder.Id] = newMedia.Id;
                    InsertSubFolders(newMedia);
                }
            }
        }

        private void ImportCmsMedia(CmsSiteEntities siteEntities)
        {
            if (siteEntities.Media != null && siteEntities.Media.Any())
            {
                _mediaMap = new ImportMap<CmsMedia>();
                foreach (var cmsMedia in TransformToTree(siteEntities.Media))
                {
                    var newMedia = CloneEntity(cmsMedia);
                    newMedia.FolderName = newMedia.FolderName + "_Copy";
                    _mediaService.Add(newMedia);
                    _mediaMap[cmsMedia.Id] = newMedia.Id;
                    InsertSubFolders(newMedia);
                }
            }

            // MediaContents
            if (siteEntities.MediaContents != null && siteEntities.MediaContents.Any())
            {
                _mediaContentsMap = new ImportMap<CmsMediaContent>();
                foreach (var mediaContent in siteEntities.MediaContents)
                {
                    var newMediaContent = CloneEntity(mediaContent);

                    newMediaContent.MediaId = _mediaMap[mediaContent.MediaId];

                    _mediaContentService.Add(newMediaContent);

                    _mediaContentsMap[mediaContent.Id] = newMediaContent.Id;
                }
            }

            // MediaFiles
            if (siteEntities.MediaFiles != null && siteEntities.MediaFiles.Any())
            {
                _mediaFilesMap = new ImportMap<CmsMediaFile>();
                foreach (var mediaFile in siteEntities.MediaFiles)
                {
                    var newMediaFile = CloneEntity(mediaFile);

                    var ext = mediaFile.LinkedFileName.Substring(mediaFile.LinkedFileName.IndexOf('.'));

                    newMediaFile.LinkedFileName = Guid.NewGuid() + ext;
                    _mediaFileService.Add(newMediaFile);
                    _mediaFilesMap[mediaFile.Id] = newMediaFile.Id;

                    var pathToFiles = CmsContext.MapPath(CmsConfiguration.AssetUploadPath);
                    File.Copy(Path.Combine(pathToFiles, mediaFile.LinkedFileName), Path.Combine(pathToFiles, newMediaFile.LinkedFileName));
                }

                foreach (var contentFileLink in siteEntities.MediaContentFileLinks)
                {
                    var newContentFileLink = CloneEntity(contentFileLink);

                    newContentFileLink.MediaContentId = _mediaContentsMap[contentFileLink.MediaContentId];
                    newContentFileLink.MediaFileId = _mediaFilesMap[contentFileLink.MediaFileId];

                    _mediaContentFileLinkService.Add(newContentFileLink);
                }
            }
        }

        private void ImportCmsTemplates(CmsSiteEntities siteEntities)
        {
            // Templates
            if (siteEntities.Templates != null && siteEntities.Templates.Any())
            {
                _templatesMap = new ImportMap<CmsTemplate>();
                foreach (var template in siteEntities.Templates)
                {
                    var newTemplate = CloneEntity(template);
                    newTemplate.SiteId = template.SiteId.HasValue ? _site.Id : new int?();
                    newTemplate.SiteGroupId = template.SiteGroupId.HasValue ? _site.SiteGroupId : new int?();
                    newTemplate.TemplateName = newTemplate.TemplateName + "_Copy";
                    _templateService.Add(newTemplate);
                    _templatesMap[template.Id] = newTemplate.Id;
                }
            }

            // Template regions
            _templateRegionsMap = new ImportMap<CmsTemplateRegion>();
            foreach (var templateRegion in siteEntities.TemplateRegions)
            {
                var template = _templateService.FindOne(_templatesMap[templateRegion.TemplateId]);
                var newTemplateRegion =
                    template.TemplateRegions.SingleOrDefault(
                        e => e.TemplateRegionName == templateRegion.TemplateRegionName);

                if (newTemplateRegion == null)
                {
                    newTemplateRegion = CloneEntity(templateRegion);
                    newTemplateRegion.TemplateId = _templatesMap[templateRegion.TemplateId];
                    _templateRegionService.Add(newTemplateRegion);
                }

                _templateRegionsMap[templateRegion.Id] = newTemplateRegion.Id;
            }
        }

        private void ImportCmsContent(CmsSiteEntities siteEntities)
        {
            // Content types
            _contentTypesMap = new ImportMap<CmsContentType>();
            var contentTypes = siteEntities.ContentTypes;
            foreach (var cmsContentType in contentTypes)
            {
                var newContentType =
                    _contentTypeService.FindAll()
                    .FirstOrDefault(e => e.ContentTypeName == cmsContentType.ContentTypeName);

                if (newContentType == null)
                {
                    newContentType = CloneEntity(cmsContentType);

                    newContentType.SiteId = cmsContentType.SiteId.HasValue ? _site.Id : new int?();
                    newContentType.SiteGroupId = cmsContentType.SiteGroupId.HasValue ? _site.SiteGroupId : new int?();

                    _contentTypeService.Add(newContentType);
                }

                _contentTypesMap[cmsContentType.Id] = newContentType.Id;
            }

            // Content
            _contentsMap = new ImportMap<CmsContent>();
            foreach (var content in siteEntities.Contents)
            {
                var newContent = CloneEntity(content);

                newContent.ContentTypeId = _contentTypesMap[content.ContentTypeId];

                _contentService.Add(newContent);

                _contentsMap[content.Id] = newContent.Id;
            }
        }

        private CmsSite ImportCmsSite(CmsSiteEntities siteEntities)
        {
            var siteGroup = _siteGroupService.FindOne(siteEntities.SiteGroup.Id);
            if (siteGroup == null)
            {
                siteGroup = new CmsSiteGroup
                {
                    SiteGroupName = siteEntities.SiteGroup.SiteGroupName
                };

                _siteGroupService.Add(siteGroup);
            }

            _site = new CmsSite
                        {
                            SiteName = siteEntities.Site.SiteName,
                            SiteGroupId = siteGroup.Id,
                            SiteGroup = siteGroup
                        };
            TrackingEntityObjectMediator.TrackEntity(_site);

            _siteService.Add(_site);
            return _site;
        }

        #endregion Private members: Import entity methods

        private static void SetRevisionableEntityState(CmsRevisionableEntityObject entityObject)
        {
            entityObject.IsLatestRevision = true;
            entityObject.IsLatestCompletedRevision = false;
            entityObject.IsDeleted = false;
            entityObject.WorkflowId = null;
        }

        private static void SetPublishedEntityState(IPublishedEntityObject entityObject)
        {
            entityObject.IsPublished = false;
            entityObject.PublishStartDate = null;
            entityObject.PublishEndDate = null;
        }

        private static T CloneEntity<T>(T entity) where T : Int32EntityObject
        {
            var newEntity = entity.FastClone();

            newEntity.Id = 0;

            if (newEntity is CmsRevisionableEntityObject)
            {
                SetRevisionableEntityState(newEntity as CmsRevisionableEntityObject);
            }

            if (newEntity is IPublishedEntityObject)
            {
                SetPublishedEntityState(newEntity as IPublishedEntityObject);
            }

            if (newEntity is ISiteAwareEntityObject)
            {
                SiteAwareEntityObjectMediator.SetSiteEntity(newEntity as ISiteAwareEntityObject);
            }

            if (newEntity is ITrackingEntityObject)
            {
                TrackingEntityObjectMediator.TrackEntity(newEntity as ITrackingEntityObject);
            }

            return newEntity;
        }

        #endregion Private members
    }

    internal class ImportMap<T>
    {
        private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>();

        public int this[int key]
        {
            get
            {
                if (!_dictionary.ContainsKey(key))
                    throw new CmsSiteImportException(String.Format("{0} with Id {1} was not found in the import package", typeof(T).Name, key));

                return _dictionary[key];
            }
            set { _dictionary[key] = value; }
        }

        public bool ContainsKey(int key)
        {
            return _dictionary.ContainsKey(key);
        }
    }

    public class CmsSiteImportException : Exception
    {
        public CmsSiteImportException(string message)
            : base(message)
        {
        }

        public CmsSiteImportException(string message, Exception innerException)
            : base(message, innerException)
        {
        }
    }
}